/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.common.media;

import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.net.Uri;
import android.view.Surface;

import com.example.android.utils.EmptyAudioResult;
import com.example.android.utils.EmptyAudioUtils;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

/**
 * Simplifies the MediaCodec interface by wrapping around the buffer processing operations.
 */
public class MediaCodecWrapper1 {

    int sum;
    double total;
    /**
     * The {@link MediaCodec} that is managed by this class.
     */
    private MediaCodec mDecoder;
    private MediaExtractor mExtractor;
    // References to the internal buffers managed by the codec. The codec
    // refers to these buffers by index, never by reference so it's up to us
    // to keep track of which buffer is which.
    private ByteBuffer[] mInputBuffers;
    private ByteBuffer[] mOutputBuffers;
    // Indices of the input buffers that are currently available for writing. We'll
    // consume these in the order they were dequeued from the codec.
    private Queue<Integer> mAvailableInputBuffers;
    // Indices of the output buffers that currently hold valid data, in the order
    // they were produced by the codec.
    private Queue<Integer> mAvailableOutputBuffers;
    // Information about each output buffer, by index. Each entry in this array
    // is valid if and only if its index is currently contained in mAvailableOutputBuffers.
    private MediaCodec.BufferInfo[] mOutputBufferInfo;

    private CompleteSampleListener mCompleteSampleListener;

    private List<short[]> mShorts = new ArrayList<>();

    private MediaCodecWrapper1(MediaCodec codec, MediaExtractor extractor) {
        mDecoder = codec;
        mExtractor = extractor;
        codec.start();
        mInputBuffers = codec.getInputBuffers();
        mOutputBuffers = codec.getOutputBuffers();
        mOutputBufferInfo = new MediaCodec.BufferInfo[mOutputBuffers.length];
        mAvailableInputBuffers = new ArrayDeque<>(mOutputBuffers.length);
        mAvailableOutputBuffers = new ArrayDeque<>(mInputBuffers.length);
    }

    /**
     * @param context The application context.
     * @param uri     The audio Uri to be decoded.
     * @return
     */
    public static MediaCodecWrapper1 fromAudioUri(Context context, Uri uri) throws IOException {
        MediaCodecWrapper1 result = null;

        // BEGIN_INCLUDE(initialize_extractor)
        MediaExtractor extractor = new MediaExtractor();
        extractor.setDataSource(context, uri, null);
        int nTracks = extractor.getTrackCount();

        // Begin by unselecting all of the tracks in the extractor, so we won't see
        // any tracks that we haven't explicitly selected.
        for (int i = 0; i < nTracks; ++i) {
            extractor.unselectTrack(i);
        }

        // Find the first video track in the stream. In a real-world application
        // it's possible that the stream would contain multiple tracks, but this
        // sample assumes that we just want to play the first one.
        for (int i = 0; i < nTracks; ++i) {
            // Try to create a video codec for this track. This call will return null if the
            // track is not a video track, or not a recognized video format. Once it returns
            // a valid MediaCodecWrapper, we can break out of the loop.
            result = MediaCodecWrapper1.fromAudioFormat(extractor, extractor.getTrackFormat(i));
            if (result != null) {
                extractor.selectTrack(i);
                break;
            }
        }

        return result;
    }

    /**
     * Constructs the {@link MediaCodecWrapper1} wrapper object around the audio codec.
     * The codec is created using the encapsulated information in the
     * {@link MediaFormat} object.
     *
     * @param trackFormat The format of the media object to be decoded.
     * @return
     */
    private static MediaCodecWrapper1 fromAudioFormat(MediaExtractor extractor, MediaFormat trackFormat) throws IOException {
        MediaCodecWrapper1 result = null;
        MediaCodec audioCodec = null;
        // BEGIN_INCLUDE(create_codec)
        final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);

        // Check to see if this is actually a video mime type. If it is, then create
        // a codec that can decode this mime type.
        if (mimeType.contains("audio/")) {
            audioCodec = MediaCodec.createDecoderByType(mimeType);
            audioCodec.configure(trackFormat, null, null, 0);
        }
        // If codec creation was successful, then create a wrapper object around the
        // newly created codec.
        if (audioCodec != null) {
            result = new MediaCodecWrapper1(audioCodec, extractor);
        }
        // END_INCLUDE(create_codec)
        return result;
    }

    public void setCompleteSampleListener(CompleteSampleListener completeSampleListener) {
        mCompleteSampleListener = completeSampleListener;
    }

    public void startDecode() {
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
                long startTime = System.currentTimeMillis();
                while (true) {
                    boolean isEos = ((mExtractor.getSampleFlags() & MediaCodec
                            .BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM);

                    // BEGIN_INCLUDE(write_sample)
                    if (!isEos) {
                        // Try to submit the sample to the codec and if successful advance the
                        // extractor to the next available sample to read.
                        boolean result = writeSample(mExtractor,
                                mExtractor.getSampleTime(), mExtractor.getSampleFlags());

                        if (result) {
                            // Advancing the extractor is a blocking operation and it MUST be
                            // executed outside the main thread in real applications.
                            mExtractor.advance();
                        }
                    }
                    // END_INCLUDE(write_sample)

                    // Examine the sample at the head of the queue to see if its ready to be
                    // rendered and is not zero sized End-of-Stream record.
                    MediaCodec.BufferInfo out_bufferInfo = new MediaCodec.BufferInfo();
                    peekSample(out_bufferInfo);

                    // BEGIN_INCLUDE(render_sample)
                    if (out_bufferInfo.size <= 0 && isEos) {
                        stopAndRelease();
                        break;
                    } else {
                        // Pop the sample off the queue and send it to {@link Surface}
                        popSample(false);
                    }
                    // END_INCLUDE(render_sample)
                }
                EmptyAudioResult result = EmptyAudioUtils.notEmptyAudio(mShorts);
                long endTime = System.currentTimeMillis();
                String content = "耗时：" + (endTime - startTime) + "毫秒";
                System.out.println(content + "  " + result.toString());
                if (mCompleteSampleListener != null) {
                    mCompleteSampleListener.completeSample(result, content);
                }
                mCompleteSampleListener = null;
                mShorts.clear();
                mShorts = null;
//            }
//        }).start();

    }

    /**
     * Write a media sample to the decoder.
     * <p>
     * A "sample" here refers to a single atomic access unit in the media stream. The definition
     * of "access unit" is dependent on the type of encoding used, but it typically refers to
     * a single frame of video or a few seconds of audio. {@link MediaExtractor}
     * extracts data from a stream one sample at a time.
     *
     * @param extractor          Instance of {@link MediaExtractor} wrapping the media.
     * @param presentationTimeUs The time, relative to the beginning of the media stream,
     *                           at which this buffer should be rendered.
     * @param flags              Flags to pass to the decoder. See {@link MediaCodec#queueInputBuffer(int,
     *                           int, int, long, int)}
     * @throws MediaCodec.CryptoException
     */
    public boolean writeSample(final MediaExtractor extractor,
                               final long presentationTimeUs,
                               int flags) {
        boolean result = false;

        if (!mAvailableInputBuffers.isEmpty()) {
            int index = mAvailableInputBuffers.remove();
            ByteBuffer buffer = mInputBuffers[index];

            // reads the sample from the file using extractor into the buffer
            int size = extractor.readSampleData(buffer, 0);
            if (size <= 0) {
                flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
            }

            // Submit the buffer to the codec for decoding. The presentationTimeUs
            // indicates the position (play time) for the current sample.
            mDecoder.queueInputBuffer(index, 0, size, presentationTimeUs, flags);

            result = true;
        }
        return result;
    }

    /**
     * Performs a peek() operation in the queue to extract media info for the buffer ready to be
     * released i.e. the head element of the queue.
     *
     * @param out_bufferInfo An output var to hold the buffer info.
     * @return True, if the peek was successful.
     */
    public boolean peekSample(MediaCodec.BufferInfo out_bufferInfo) {
        // dequeue available buffers and synchronize our data structures with the codec.
        update();
        boolean result = false;
        if (!mAvailableOutputBuffers.isEmpty()) {
            int index = mAvailableOutputBuffers.peek();
            MediaCodec.BufferInfo info = mOutputBufferInfo[index];
            // metadata of the sample
            out_bufferInfo.set(
                    info.offset,
                    info.size,
                    info.presentationTimeUs,
                    info.flags);
            result = true;
        }
        return result;
    }

    /**
     * Releases resources and ends the encoding/decoding session.
     */
    public void stopAndRelease() {
        mDecoder.stop();
        mDecoder.release();
        mExtractor.release();
        mExtractor = null;
        mDecoder = null;
    }

    /**
     * Processes, releases and optionally renders the output buffer available at the head of the
     * queue.
     * @param render True, if the buffer is to be rendered on the {@link Surface} configured
     */
    public void popSample(boolean render) {
        // dequeue available buffers and synchronize our data structures with the codec.
        update();
        if (!mAvailableOutputBuffers.isEmpty()) {
            int index = mAvailableOutputBuffers.remove();
            ByteBuffer outputBuffer = mOutputBuffers[index];
            ShortBuffer shortBuffer = outputBuffer.asShortBuffer();
            short[] dst = new short[shortBuffer.remaining()];
            shortBuffer.get(dst);
            mShorts.add(dst);
            // releases the buffer back to the codec
            mDecoder.releaseOutputBuffer(index, render);
        }
    }

    /**
     * Synchronize this object's state with the internal state of the wrapped
     * MediaCodec.
     */
    private void update() {
        // BEGIN_INCLUDE(update_codec_state)
        int index;

        // Get valid input buffers from the codec to fill later in the same order they were
        // made available by the codec.
        while ((index = mDecoder.dequeueInputBuffer(0)) != MediaCodec.INFO_TRY_AGAIN_LATER) {
            mAvailableInputBuffers.add(index);
        }


        // Likewise with output buffers. If the output buffers have changed, start using the
        // new set of output buffers. If the output format has changed, notify listeners.
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        while ((index = mDecoder.dequeueOutputBuffer(info, 0)) != MediaCodec.INFO_TRY_AGAIN_LATER) {
            switch (index) {
                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                    mOutputBuffers = mDecoder.getOutputBuffers();
                    mOutputBufferInfo = new MediaCodec.BufferInfo[mOutputBuffers.length];
                    mAvailableOutputBuffers.clear();
                    break;
                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                    //先不做处理
                    break;
                default:
                    // Making sure the index is valid before adding to output buffers. We've already
                    // handled INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED &
                    // INFO_OUTPUT_BUFFERS_CHANGED i.e all the other possible return codes but
                    // asserting index value anyways for future-proofing the code.
                    if (index >= 0) {
                        mOutputBufferInfo[index] = info;
                        mAvailableOutputBuffers.add(index);
                    } else {
                        throw new IllegalStateException("Unknown status from dequeueOutputBuffer");
                    }
                    break;
            }

        }
        // END_INCLUDE(update_codec_state)

    }

    public interface CompleteSampleListener {
        void completeSample(EmptyAudioResult result, String content);
    }
}
